{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GuueVhEpUfIe"
      },
      "source": [
        "# Importing libraries"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Re-pZPrtUfIj"
      },
      "outputs": [],
      "source": [
        "import torch\n",
        "import torch.nn as nn\n",
        "import torch.optim as optim\n",
        "import torch.nn.functional as F\n",
        "import torchvision\n",
        "import torchvision.models as models\n",
        "import torchvision.transforms as transforms\n",
        "import torchvision.datasets as datasets\n",
        "\n",
        "import time\n",
        "import matplotlib\n",
        "from matplotlib import pyplot as plt\n",
        "import copy"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "tNMhS3wgUfIn"
      },
      "source": [
        "# Installing DCLS and importing Dcls2d"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "4F8tZVkEUfIo",
        "outputId": "39b79c07-2e87-4a2d-978a-d5adb492ffb7"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
            "Collecting dcls\n",
            "  Downloading dcls-0.0.5-py3-none-any.whl (11 kB)\n",
            "Installing collected packages: dcls\n",
            "Successfully installed dcls-0.0.5\n"
          ]
        }
      ],
      "source": [
        "!pip3 install dcls\n",
        "from DCLS.construct.modules import  Dcls2d"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "k1Eyk827UfIp"
      },
      "source": [
        "# Fine-tuning a ConvNeXt-T model on STL-10 dataset"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "L1fumeXxUfIp",
        "outputId": "cf006e23-d3c0-436c-f492-fa4801495cde"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Downloading http://ai.stanford.edu/~acoates/stl10/stl10_binary.tar.gz to ./data/stl10_binary.tar.gz\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "100%|██████████| 2640397119/2640397119 [02:38<00:00, 16614300.68it/s]\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Extracting ./data/stl10_binary.tar.gz to ./data\n",
            "Files already downloaded and verified\n"
          ]
        }
      ],
      "source": [
        "# Set device to GPU if available\n",
        "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
        "\n",
        "# Define transforms for the dataset\n",
        "transform = transforms.Compose([\n",
        "    transforms.RandomCrop(96, padding=4),\n",
        "    transforms.RandomHorizontalFlip(),\n",
        "    transforms.ToTensor(),\n",
        "    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))\n",
        "])\n",
        "\n",
        "# Load STL-10 dataset\n",
        "train_dataset = datasets.STL10(root='./data', split='train', download=True, transform=transform)\n",
        "test_dataset = datasets.STL10(root='./data', split='test', download=True, transform=transform)\n",
        "\n",
        "# Create data loaders\n",
        "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True)\n",
        "test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=128, shuffle=False)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "flwLmy2XUfIq"
      },
      "outputs": [],
      "source": [
        "# ConvNeXt model\n",
        "class Block(nn.Module):\n",
        "    r\"\"\" ConvNeXt Block. There are two equivalent implementations:\n",
        "    (1) DwConv -> LayerNorm (channels_first) -> 1x1 Conv -> GELU -> 1x1 Conv; all in (N, C, H, W)\n",
        "    (2) DwConv -> Permute to (N, H, W, C); LayerNorm (channels_last) -> Linear -> GELU -> Linear; Permute back\n",
        "    We use (2) as we find it slightly faster in PyTorch\n",
        "\n",
        "    Args:\n",
        "        dim (int): Number of input channels.\n",
        "        drop_path (float): Stochastic depth rate. Default: 0.0\n",
        "        layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.\n",
        "    \"\"\"\n",
        "    def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6):\n",
        "        super().__init__()\n",
        "        self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim) # depthwise conv\n",
        "        self.norm = LayerNorm(dim, eps=1e-6)\n",
        "        self.pwconv1 = nn.Linear(dim, 4 * dim) # pointwise/1x1 convs, implemented with linear layers\n",
        "        self.act = nn.GELU()\n",
        "        self.pwconv2 = nn.Linear(4 * dim, dim)\n",
        "        self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)),\n",
        "                                    requires_grad=True) if layer_scale_init_value > 0 else None\n",
        "        self.drop_path = torchvision.ops.StochasticDepth(p=drop_path, mode='row') if drop_path > 0. else nn.Identity()\n",
        "\n",
        "    def forward(self, x):\n",
        "        input = x\n",
        "        x = self.dwconv(x)\n",
        "        x = x.permute(0, 2, 3, 1) # (N, C, H, W) -> (N, H, W, C)\n",
        "        x = self.norm(x)\n",
        "        x = self.pwconv1(x)\n",
        "        x = self.act(x)\n",
        "        x = self.pwconv2(x)\n",
        "        if self.gamma is not None:\n",
        "            x = self.gamma * x\n",
        "        x = x.permute(0, 3, 1, 2) # (N, H, W, C) -> (N, C, H, W)\n",
        "\n",
        "        x = input + self.drop_path(x)\n",
        "        return x\n",
        "\n",
        "class ConvNeXt(nn.Module):\n",
        "    r\"\"\" ConvNeXt\n",
        "        A PyTorch impl of : `A ConvNet for the 2020s`  -\n",
        "          https://arxiv.org/pdf/2201.03545.pdf\n",
        "\n",
        "    Args:\n",
        "        in_chans (int): Number of input image channels. Default: 3\n",
        "        num_classes (int): Number of classes for classification head. Default: 1000\n",
        "        depths (tuple(int)): Number of blocks at each stage. Default: [3, 3, 9, 3]\n",
        "        dims (int): Feature dimension at each stage. Default: [96, 192, 384, 768]\n",
        "        drop_path_rate (float): Stochastic depth rate. Default: 0.\n",
        "        layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.\n",
        "        head_init_scale (float): Init scaling value for classifier weights and biases. Default: 1.\n",
        "    \"\"\"\n",
        "    def __init__(self, in_chans=3, num_classes=1000,\n",
        "                 depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], drop_path_rate=0.,\n",
        "                 layer_scale_init_value=1e-6, head_init_scale=1.,\n",
        "                 ):\n",
        "        super().__init__()\n",
        "\n",
        "        self.downsample_layers = nn.ModuleList() # stem and 3 intermediate downsampling conv layers\n",
        "        stem = nn.Sequential(\n",
        "            nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4),\n",
        "            LayerNorm(dims[0], eps=1e-6, data_format=\"channels_first\")\n",
        "        )\n",
        "        self.downsample_layers.append(stem)\n",
        "        for i in range(3):\n",
        "            downsample_layer = nn.Sequential(\n",
        "                    LayerNorm(dims[i], eps=1e-6, data_format=\"channels_first\"),\n",
        "                    nn.Conv2d(dims[i], dims[i+1], kernel_size=2, stride=2),\n",
        "            )\n",
        "            self.downsample_layers.append(downsample_layer)\n",
        "\n",
        "        self.stages = nn.ModuleList() # 4 feature resolution stages, each consisting of multiple residual blocks\n",
        "        dp_rates=[x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]\n",
        "        cur = 0\n",
        "        for i in range(4):\n",
        "            stage = nn.Sequential(\n",
        "                *[Block(dim=dims[i], drop_path=dp_rates[cur + j],\n",
        "                layer_scale_init_value=layer_scale_init_value) for j in range(depths[i])]\n",
        "            )\n",
        "            self.stages.append(stage)\n",
        "            cur += depths[i]\n",
        "\n",
        "        self.norm = nn.LayerNorm(dims[-1], eps=1e-6) # final norm layer\n",
        "        self.head = nn.Linear(dims[-1], num_classes)\n",
        "\n",
        "        self.apply(self._init_weights)\n",
        "        self.head.weight.data.mul_(head_init_scale)\n",
        "        self.head.bias.data.mul_(head_init_scale)\n",
        "\n",
        "    def _init_weights(self, m):\n",
        "        if isinstance(m, (nn.Conv2d, nn.Linear)):\n",
        "            nn.init.normal_(m.weight, std=.02)\n",
        "            nn.init.constant_(m.bias, 0)\n",
        "\n",
        "    def forward_features(self, x):\n",
        "        for i in range(4):\n",
        "            x = self.downsample_layers[i](x)\n",
        "            x = self.stages[i](x)\n",
        "        return self.norm(x.mean([-2, -1])) # global average pooling, (N, C, H, W) -> (N, C)\n",
        "\n",
        "    def forward(self, x):\n",
        "        x = self.forward_features(x)\n",
        "        x = self.head(x)\n",
        "        return x\n",
        "\n",
        "class LayerNorm(nn.Module):\n",
        "    r\"\"\" LayerNorm that supports two data formats: channels_last (default) or channels_first.\n",
        "    The ordering of the dimensions in the inputs. channels_last corresponds to inputs with\n",
        "    shape (batch_size, height, width, channels) while channels_first corresponds to inputs\n",
        "    with shape (batch_size, channels, height, width).\n",
        "    \"\"\"\n",
        "    def __init__(self, normalized_shape, eps=1e-6, data_format=\"channels_last\"):\n",
        "        super().__init__()\n",
        "        self.weight = nn.Parameter(torch.ones(normalized_shape))\n",
        "        self.bias = nn.Parameter(torch.zeros(normalized_shape))\n",
        "        self.eps = eps\n",
        "        self.data_format = data_format\n",
        "        if self.data_format not in [\"channels_last\", \"channels_first\"]:\n",
        "            raise NotImplementedError\n",
        "        self.normalized_shape = (normalized_shape, )\n",
        "\n",
        "    def forward(self, x):\n",
        "        if self.data_format == \"channels_last\":\n",
        "            return F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps)\n",
        "        elif self.data_format == \"channels_first\":\n",
        "            u = x.mean(1, keepdim=True)\n",
        "            s = (x - u).pow(2).mean(1, keepdim=True)\n",
        "            x = (x - u) / torch.sqrt(s + self.eps)\n",
        "            x = self.weight[:, None, None] * x + self.bias[:, None, None]\n",
        "            return x\n",
        "\n",
        "\n",
        "model_urls = {\n",
        "    \"convnext_tiny_1k\": \"https://dl.fbaipublicfiles.com/convnext/convnext_tiny_1k_224_ema.pth\",\n",
        "    \"convnext_dcls_gauss_tiny_1k\": \"https://zenodo.org/record/8029747/files/convnext_dcls_gauss_tiny_1k_224_ema.pth\",\n",
        "}\n",
        "\n",
        "def load_checkpoint(model, checkpoint_name='convnext_tiny_1k'):\n",
        "    url = model_urls[checkpoint_name]\n",
        "    checkpoint = torch.hub.load_state_dict_from_url(url=url, map_location=\"cpu\", check_hash=True)\n",
        "    print(model.load_state_dict(checkpoint[\"model\"]))\n",
        "\n",
        "def convnext_tiny(pretrained=False, **kwargs):\n",
        "    model = ConvNeXt(depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], **kwargs)\n",
        "    if pretrained:\n",
        "        url = model_urls['convnext_tiny_1k']\n",
        "        checkpoint = torch.hub.load_state_dict_from_url(url=url, map_location=\"cpu\", check_hash=True)\n",
        "        model.load_state_dict(checkpoint[\"model\"])\n",
        "    return model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Zl4Ml-YJUfIs",
        "outputId": "e40caf17-b047-441b-ec06-3ccca6ecf5f8"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Number of parameters of the model: 28.59 M\n"
          ]
        }
      ],
      "source": [
        "# Load pre-trained ConvNeXt-T model\n",
        "model = convnext_tiny(pretrained=True)\n",
        "n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
        "print(f\"Number of parameters of the model: {n_parameters/1e6:.2f} M\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "vDVDDQ_pUfIt"
      },
      "outputs": [],
      "source": [
        "# Freeze all the layers except the last fully connected layer\n",
        "for param in model.parameters():\n",
        "    param.requires_grad = False"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "pzQ8hDgaUfIu",
        "outputId": "9dd02661-312a-4523-8b80-b05f5a1bf733"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "ConvNeXt(\n",
              "  (downsample_layers): ModuleList(\n",
              "    (0): Sequential(\n",
              "      (0): Conv2d(3, 96, kernel_size=(4, 4), stride=(4, 4))\n",
              "      (1): LayerNorm()\n",
              "    )\n",
              "    (1): Sequential(\n",
              "      (0): LayerNorm()\n",
              "      (1): Conv2d(96, 192, kernel_size=(2, 2), stride=(2, 2))\n",
              "    )\n",
              "    (2): Sequential(\n",
              "      (0): LayerNorm()\n",
              "      (1): Conv2d(192, 384, kernel_size=(2, 2), stride=(2, 2))\n",
              "    )\n",
              "    (3): Sequential(\n",
              "      (0): LayerNorm()\n",
              "      (1): Conv2d(384, 768, kernel_size=(2, 2), stride=(2, 2))\n",
              "    )\n",
              "  )\n",
              "  (stages): ModuleList(\n",
              "    (0): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Conv2d(96, 96, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=96)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=96, out_features=384, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=384, out_features=96, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Conv2d(96, 96, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=96)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=96, out_features=384, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=384, out_features=96, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Conv2d(96, 96, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=96)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=96, out_features=384, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=384, out_features=96, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "    (1): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Conv2d(192, 192, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=192)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=192, out_features=768, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=768, out_features=192, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Conv2d(192, 192, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=192)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=192, out_features=768, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=768, out_features=192, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Conv2d(192, 192, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=192)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=192, out_features=768, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=768, out_features=192, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "    (2): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (3): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (4): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (5): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (6): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (7): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (8): Block(\n",
              "        (dwconv): Conv2d(384, 384, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=384)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "    (3): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Conv2d(768, 768, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=768)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=768, out_features=3072, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=3072, out_features=768, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Conv2d(768, 768, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=768)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=768, out_features=3072, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=3072, out_features=768, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Conv2d(768, 768, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=768)\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=768, out_features=3072, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=3072, out_features=768, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "  )\n",
              "  (norm): LayerNorm((768,), eps=1e-06, elementwise_affine=True)\n",
              "  (head): Linear(in_features=768, out_features=10, bias=True)\n",
              ")"
            ]
          },
          "metadata": {},
          "execution_count": 23
        }
      ],
      "source": [
        "# Replace the last fully connected layer to match the number of classes in STL-10\n",
        "num_classes = 10\n",
        "model.head = nn.Linear(768, num_classes)\n",
        "model.to(device)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "q743FVmsUfIv"
      },
      "outputs": [],
      "source": [
        "# Define loss function and optimizer\n",
        "criterion = nn.CrossEntropyLoss()\n",
        "optimizer = optim.Adam(model.head.parameters(), lr=0.001)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "EOMobZUyUfIv"
      },
      "outputs": [],
      "source": [
        "# Define a train loop\n",
        "def train_and_test(model, criterion, optimizer):\n",
        "  start_time = time.time()\n",
        "  # Training loop\n",
        "  num_epochs = 10\n",
        "  stats_dict = dict(train_loss = [], train_acc = [], test_loss = [], test_acc = [])\n",
        "  for epoch in range(num_epochs):\n",
        "      model.train()  # Set model to training mode\n",
        "      running_loss = 0.0\n",
        "      correct = 0\n",
        "      total = 0\n",
        "\n",
        "      for images, labels in train_loader:\n",
        "          images = images.to(device)\n",
        "          labels = labels.to(device)\n",
        "\n",
        "          optimizer.zero_grad()\n",
        "\n",
        "          outputs = model(images)\n",
        "          loss = criterion(outputs, labels)\n",
        "          loss.backward()\n",
        "          optimizer.step()\n",
        "\n",
        "          running_loss += loss.item()\n",
        "          _, predicted = outputs.max(1)\n",
        "          total += labels.size(0)\n",
        "          correct += predicted.eq(labels).sum().item()\n",
        "\n",
        "      train_loss = running_loss / len(train_loader)\n",
        "      train_acc = 100.0 * correct / total\n",
        "\n",
        "      # Evaluate on the test set\n",
        "      model.eval()  # Set model to evaluation mode\n",
        "      test_loss = 0.0\n",
        "      correct = 0\n",
        "      total = 0\n",
        "\n",
        "      with torch.no_grad():\n",
        "          for images, labels in test_loader:\n",
        "              images = images.to(device)\n",
        "              labels = labels.to(device)\n",
        "\n",
        "              outputs = model(images)\n",
        "              loss = criterion(outputs, labels)\n",
        "\n",
        "              test_loss += loss.item()\n",
        "              _, predicted = outputs.max(1)\n",
        "              total += labels.size(0)\n",
        "              correct += predicted.eq(labels).sum().item()\n",
        "\n",
        "      test_loss /= len(test_loader)\n",
        "      test_acc = 100.0 * correct / total\n",
        "\n",
        "      # Print epoch statistics\n",
        "      stats_dict['train_loss'].append(train_loss); stats_dict['train_acc'].append(train_acc)\n",
        "      stats_dict['test_loss'].append(test_loss); stats_dict['test_acc'].append(test_acc)\n",
        "      print(f'Epoch [{epoch+1}/{num_epochs}]: Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}% | '\n",
        "            f'Test Loss: {test_loss:.4f} | Test Acc: {test_acc:.2f}%')\n",
        "\n",
        "  end_time = time.time()\n",
        "  elapsed_time = end_time - start_time\n",
        "  print(f'Elapsed time: {elapsed_time:.2f} (s)')\n",
        "\n",
        "  return stats_dict"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "JhL8psXRUfIw",
        "outputId": "83f9f7a9-2829-4226-cdea-f670dd061d9c"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Epoch [1/10]: Train Loss: 1.2787 | Train Acc: 84.22% | Test Loss: 0.6499 | Test Acc: 94.38%\n",
            "Epoch [2/10]: Train Loss: 0.4633 | Train Acc: 94.78% | Test Loss: 0.3624 | Test Acc: 95.22%\n",
            "Epoch [3/10]: Train Loss: 0.3038 | Train Acc: 95.64% | Test Loss: 0.2709 | Test Acc: 95.44%\n",
            "Epoch [4/10]: Train Loss: 0.2327 | Train Acc: 96.08% | Test Loss: 0.2305 | Test Acc: 95.45%\n",
            "Epoch [5/10]: Train Loss: 0.1998 | Train Acc: 95.96% | Test Loss: 0.2042 | Test Acc: 95.92%\n",
            "Epoch [6/10]: Train Loss: 0.1808 | Train Acc: 96.14% | Test Loss: 0.1831 | Test Acc: 95.89%\n",
            "Epoch [7/10]: Train Loss: 0.1576 | Train Acc: 96.58% | Test Loss: 0.1718 | Test Acc: 96.06%\n",
            "Epoch [8/10]: Train Loss: 0.1429 | Train Acc: 96.90% | Test Loss: 0.1649 | Test Acc: 95.94%\n",
            "Epoch [9/10]: Train Loss: 0.1352 | Train Acc: 96.90% | Test Loss: 0.1578 | Test Acc: 95.84%\n",
            "Epoch [10/10]: Train Loss: 0.1311 | Train Acc: 96.92% | Test Loss: 0.1517 | Test Acc: 95.88%\n",
            "Elapsed time: 201.00 (s)\n"
          ]
        }
      ],
      "source": [
        "# train and test the model\n",
        "convnext_tiny_stats = train_and_test(model, criterion, optimizer)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "H2Af6O3fUfIw"
      },
      "source": [
        "# Fine-tuning a ConvNeXt-T-DCLS model on STL-10 dataset"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "wZo6aoBfUfIx"
      },
      "outputs": [],
      "source": [
        "# Helper function that replaces all \".int.\" patterns\n",
        "# by \"[int]\" in a character string\n",
        "def replace_dots_brackets(name):\n",
        "  name_split = name.split('.')\n",
        "  name_split = ['[' + i + ']' if i.isdigit() else '.' + i  for i in name_split]\n",
        "\n",
        "  return ''.join(name_split[:-1]), name_split[-1][1:]\n",
        "\n",
        "# Helper function that replaces all the\n",
        "# 2D depthwise separable convolution in\n",
        "# a model by synchronized Dcls2d ones\n",
        "def replace_depthwise_dcls(model, dilated_kernel_size = 23, kernel_count = 26, version = 'gauss'):\n",
        "  in_channels, P, SIG = 0, None, None\n",
        "  # Loop over all model modules\n",
        "  for name, module in model.named_modules():\n",
        "      # if the module is a depthwise separable Conv2d module\n",
        "      if isinstance(module, nn.Conv2d) and module.groups == module.in_channels:\n",
        "          name_eval, last_layer = replace_dots_brackets(name)\n",
        "          dcls_conv = Dcls2d(module.in_channels, module.out_channels,\n",
        "                           kernel_count=kernel_count,\n",
        "                           dilated_kernel_size=dilated_kernel_size,\n",
        "                           padding=dilated_kernel_size//2,\n",
        "                           groups=module.in_channels, version=version)\n",
        "\n",
        "          nn.init.normal_(dcls_conv.weight, std=.02)\n",
        "          nn.init.constant_(dcls_conv.bias, 0)\n",
        "\n",
        "          # Synchronise positions and standard\n",
        "          # deviations belonging to the same stage\n",
        "          if in_channels < module.in_channels :\n",
        "            in_channels = module.in_channels\n",
        "            P, SIG = dcls_conv.P, dcls_conv.SIG\n",
        "\n",
        "          dcls_conv.P, dcls_conv.SIG = P, SIG\n",
        "\n",
        "          setattr(eval(\"model\" + name_eval), last_layer, dcls_conv)\n",
        "  return model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "_mZS0s0yUfIx"
      },
      "outputs": [],
      "source": [
        "# Load an empty ConvNeXt-T model\n",
        "model = convnext_tiny()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "YxfvDF-lUfIy",
        "outputId": "6a56223a-dfcc-46d6-cf73-1be45a98d88e"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n",
            "WARNING:root:DepthWiseConv2dImplicitGEMM not installed\n",
            "WARNING:root:switching to native conv2d\n"
          ]
        }
      ],
      "source": [
        "# Replace all the 2D depthwise separable convolutions\n",
        "# in the model by synchronized Dcls2d ones. Here, we\n",
        "# use a dilated kernel size of 23, a kernel count of\n",
        "# 26 (to stay at iso-paramters with the baseline)\n",
        "# and Gaussian interpolation\n",
        "model_dcls = replace_depthwise_dcls(copy.deepcopy(model),\n",
        "                                    dilated_kernel_size = 23,\n",
        "                                    kernel_count = 26, version = 'gauss')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "795Fz_iJUfIz",
        "outputId": "675c530f-2488-4457-b767-78444e1291e8"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "<All keys matched successfully>\n",
            "Number of parameters of the model: 28.59 M\n"
          ]
        }
      ],
      "source": [
        "# Load pre-trained ConvNeXt-T-DCLS model\n",
        "load_checkpoint(model_dcls, 'convnext_dcls_gauss_tiny_1k')\n",
        "n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
        "print(f\"Number of parameters of the model: {n_parameters/1e6:.2f} M\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "vb2M3mO5UfIz"
      },
      "outputs": [],
      "source": [
        "# Freeze all the layers except the last fully connected layer\n",
        "for param in model_dcls.parameters():\n",
        "    param.requires_grad = False"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Te1-5mqBUfIz",
        "outputId": "84bfaa4a-a959-4ade-c83e-ed54ce96ffcb"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "ConvNeXt(\n",
              "  (downsample_layers): ModuleList(\n",
              "    (0): Sequential(\n",
              "      (0): Conv2d(3, 96, kernel_size=(4, 4), stride=(4, 4))\n",
              "      (1): LayerNorm()\n",
              "    )\n",
              "    (1): Sequential(\n",
              "      (0): LayerNorm()\n",
              "      (1): Conv2d(96, 192, kernel_size=(2, 2), stride=(2, 2))\n",
              "    )\n",
              "    (2): Sequential(\n",
              "      (0): LayerNorm()\n",
              "      (1): Conv2d(192, 384, kernel_size=(2, 2), stride=(2, 2))\n",
              "    )\n",
              "    (3): Sequential(\n",
              "      (0): LayerNorm()\n",
              "      (1): Conv2d(384, 768, kernel_size=(2, 2), stride=(2, 2))\n",
              "    )\n",
              "  )\n",
              "  (stages): ModuleList(\n",
              "    (0): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          96, 96, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=96, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(96, 96, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=96)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=96, out_features=384, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=384, out_features=96, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          96, 96, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=96, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(96, 96, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=96)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=96, out_features=384, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=384, out_features=96, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          96, 96, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=96, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(96, 96, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=96)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=96, out_features=384, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=384, out_features=96, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "    (1): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          192, 192, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=192, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(192, 192, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=192)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=192, out_features=768, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=768, out_features=192, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          192, 192, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=192, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(192, 192, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=192)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=192, out_features=768, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=768, out_features=192, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          192, 192, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=192, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(192, 192, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=192)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=192, out_features=768, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=768, out_features=192, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "    (2): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (3): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (4): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (5): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (6): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (7): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (8): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          384, 384, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=384, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(384, 384, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=384)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=384, out_features=1536, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=1536, out_features=384, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "    (3): Sequential(\n",
              "      (0): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          768, 768, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=768, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(768, 768, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=768)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=768, out_features=3072, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=3072, out_features=768, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (1): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          768, 768, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=768, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(768, 768, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=768)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=768, out_features=3072, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=3072, out_features=768, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "      (2): Block(\n",
              "        (dwconv): Dcls2d(\n",
              "          768, 768, kernel_count=26 (previous kernel_size), stride=(1, 1), version=gauss, padding=(11, 11), dilated_kernel_size=(23, 23) (learnable), groups=768, (using torch im2col GEMM)\n",
              "          (DCK): ConstructKernel2d(768, 768, kernel_count=26, version=gauss, dilated_kernel_size=(23, 23), groups=768)\n",
              "        )\n",
              "        (norm): LayerNorm()\n",
              "        (pwconv1): Linear(in_features=768, out_features=3072, bias=True)\n",
              "        (act): GELU(approximate='none')\n",
              "        (pwconv2): Linear(in_features=3072, out_features=768, bias=True)\n",
              "        (drop_path): Identity()\n",
              "      )\n",
              "    )\n",
              "  )\n",
              "  (norm): LayerNorm((768,), eps=1e-06, elementwise_affine=True)\n",
              "  (head): Linear(in_features=768, out_features=10, bias=True)\n",
              ")"
            ]
          },
          "metadata": {},
          "execution_count": 32
        }
      ],
      "source": [
        "# Replace the last fully connected layer to match the number of classes in STL-10\n",
        "num_classes = 10\n",
        "model_dcls.head = nn.Linear(768, num_classes)\n",
        "model_dcls.to(device)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ALJlgFBcUfI0"
      },
      "outputs": [],
      "source": [
        "# Define loss function and optimizer\n",
        "criterion = nn.CrossEntropyLoss()\n",
        "optimizer = optim.Adam(model_dcls.head.parameters(), lr=0.001)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "KtSLRbVoUfI0",
        "outputId": "0a6437c2-ab39-4fad-88ec-609e9b7f349a"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Epoch [1/10]: Train Loss: 1.1954 | Train Acc: 84.66% | Test Loss: 0.5591 | Test Acc: 94.05%\n",
            "Epoch [2/10]: Train Loss: 0.4010 | Train Acc: 95.08% | Test Loss: 0.3136 | Test Acc: 95.19%\n",
            "Epoch [3/10]: Train Loss: 0.2589 | Train Acc: 95.54% | Test Loss: 0.2378 | Test Acc: 95.28%\n",
            "Epoch [4/10]: Train Loss: 0.2033 | Train Acc: 96.00% | Test Loss: 0.2012 | Test Acc: 95.60%\n",
            "Epoch [5/10]: Train Loss: 0.1815 | Train Acc: 96.22% | Test Loss: 0.1771 | Test Acc: 95.92%\n",
            "Epoch [6/10]: Train Loss: 0.1604 | Train Acc: 96.46% | Test Loss: 0.1658 | Test Acc: 96.08%\n",
            "Epoch [7/10]: Train Loss: 0.1407 | Train Acc: 96.76% | Test Loss: 0.1557 | Test Acc: 96.24%\n",
            "Epoch [8/10]: Train Loss: 0.1343 | Train Acc: 96.66% | Test Loss: 0.1486 | Test Acc: 96.09%\n",
            "Epoch [9/10]: Train Loss: 0.1228 | Train Acc: 97.08% | Test Loss: 0.1422 | Test Acc: 96.17%\n",
            "Epoch [10/10]: Train Loss: 0.1097 | Train Acc: 97.20% | Test Loss: 0.1373 | Test Acc: 96.33%\n",
            "Elapsed time: 312.37 (s)\n"
          ]
        }
      ],
      "source": [
        "# train and test the model\n",
        "convnext_dcls_gauss_tiny_stats = train_and_test(model_dcls, criterion, optimizer)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 472
        },
        "id": "xlMBx63OUfI0",
        "outputId": "97f8025a-81a9-4f4f-b71f-a20aa2484f7d"
      },
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<Figure size 640x480 with 4 Axes>"
            ],
            "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAHHCAYAAABXx+fLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACp3ElEQVR4nOzdd3xT9f748VeStuneu5S2lFH2EioIOECRiyDDAQ4URX9XRa9y1Xu598pQlHu5il4V0a8DcSEiCIoKAqKILNkbSmmhpXSPdI/k/P44aUpoC91p2vfz8TiPJGflHSAf3vlMjaIoCkIIIYQQ7YjW1gEIIYQQQrQ0SYCEEEII0e5IAiSEEEKIdkcSICGEEEK0O5IACSGEEKLdkQRICCGEEO2OJEBCCCGEaHckARJCCCFEuyMJkBBCCCHaHUmARKsWGRnJgw8+2Cz3/vjjj9FoNCQmJjbL/YUQbYNGo2HevHm2DkM0MUmARKPt2LGDefPmkZuba+tQhBB2qCXKkFdeeYW1a9c22/2F/ZEESDTajh07mD9/frMUXqdOneL9999v8vsKIVqP5ixDKkkCJC4nCZBoMSaTiZKSknpdo9frcXR0bKaIhBBCtFeSAIlGmTdvHs899xwAUVFRaDQaS78ajUbDzJkz+fzzz+nZsyd6vZ4NGzYA8OqrrzJ06FD8/PxwcXFh4MCBfP3119Xuf3kfoMp+O7///juzZs0iICAANzc3Jk6cSEZGRpN8pnfeeccSb2hoKE888US1X6ZxcXFMnjyZ4OBgnJ2d6dChA1OmTCEvL89yzqZNmxg2bBje3t64u7vTrVs3/vGPfzRJjEK0FVcqQwA+++wzBg4ciIuLC76+vkyZMoWkpCSre1zt+6jRaCgsLGT58uWW+ze2b+GBAwcYM2YMnp6euLu7M3LkSHbt2mV1Tnl5OfPnz6dLly44Ozvj5+fHsGHD2LRpk+Wc1NRUpk+fTocOHdDr9YSEhHD77bdL38QW4GDrAIR9mzRpEqdPn2bFihW8/vrr+Pv7AxAQEADAzz//zFdffcXMmTPx9/cnMjISgP/973+MHz+ee++9l7KyMr788kvuvPNO1q9fz9ixY6/6vk8++SQ+Pj7MnTuXxMRE3njjDWbOnMnKlSsb9XnmzZvH/PnzGTVqFI899hinTp1i6dKl/PHHH/z+++84OjpSVlbG6NGjKS0t5cknnyQ4OJgLFy6wfv16cnNz8fLy4tixY9x222306dOHF198Eb1ez5kzZ/j9998bFZ8Qbc2VypCXX36ZF154gbvuuosZM2aQkZHBW2+9xYgRIzhw4ADe3t51+j5++umnzJgxg8GDB/Poo48CEB0d3eCYjx07xvDhw/H09OT555/H0dGR9957jxtuuIFff/2V2NhYQC1PFi5caHlvg8HA3r172b9/PzfffDMAkydP5tixYzz55JNERkaSnp7Opk2bOH/+vKW8FM1EEaKR/vvf/yqAkpCQYLUfULRarXLs2LFq1xQVFVm9LisrU3r16qXcdNNNVvsjIiKUBx54wPJ62bJlCqCMGjVKMZlMlv3PPPOMotPplNzc3DrHXXmvyrjT09MVJycn5ZZbblGMRqPlvLffflsBlI8++khRFEU5cOCAAiirVq2q9d6vv/66AigZGRl1jkeI9qqmMiQxMVHR6XTKyy+/bHXukSNHFAcHB8v+unwfFUVR3NzcrMqS+gCUuXPnWl5PmDBBcXJyUuLj4y37UlJSFA8PD2XEiBGWfX379lXGjh1b631zcnIUQPnvf//boLhE40gTmGhW119/PT169Ki238XFxfI8JyeHvLw8hg8fzv79++t030cffRSNRmN5PXz4cIxGI+fOnWtwrJs3b6asrIynn34arbbqq/HII4/g6enJ999/D4CXlxcAGzdupKioqMZ7eXt7A7Bu3TpMJlODYxKivVqzZg0mk4m77rqLzMxMyxYcHEyXLl3YunUrULfvY1MyGo389NNPTJgwgU6dOln2h4SEcM8997B9+3YMBgOglgPHjh0jLi6uxnu5uLjg5OTEL7/8Qk5OTrPHLqxJAiSaVVRUVI37169fz7XXXouzszO+vr4EBASwdOlSqz40V9KxY0er1z4+PgCNKkQqk6du3bpZ7XdycqJTp06W41FRUcyaNYsPPvgAf39/Ro8ezZIlS6xiv/vuu7nuuuuYMWMGQUFBTJkyha+++kqSISHqKC4uDkVR6NKlCwEBAVbbiRMnSE9PB+r2fWxKGRkZFBUVVSsnALp3747JZLL0UXrxxRfJzc2la9eu9O7dm+eee47Dhw9bztfr9fznP//hxx9/JCgoiBEjRrBo0SJSU1ObJXZhTRIg0awuremp9NtvvzF+/HicnZ155513+OGHH9i0aRP33HMPiqLU6b46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\n"
          },
          "metadata": {}
        }
      ],
      "source": [
        "fig, axs = plt.subplots(2, 2)\n",
        "axs[0, 0].plot(convnext_tiny_stats['train_loss'], label='convnext-t')\n",
        "axs[0, 0].plot(convnext_dcls_gauss_tiny_stats['train_loss'], label='convnext-dcls-t')\n",
        "axs[0, 0].set_title('train_loss')\n",
        "axs[0, 1].plot(convnext_tiny_stats['test_loss'], label='convnext-t')\n",
        "axs[0, 1].plot(convnext_dcls_gauss_tiny_stats['test_loss'], label='convnext-dcls-t')\n",
        "axs[0, 1].set_title('test_loss')\n",
        "axs[1, 0].plot(convnext_tiny_stats['train_acc'], label='convnext-t')\n",
        "axs[1, 0].plot(convnext_dcls_gauss_tiny_stats['train_acc'], label='convnext-dcls-t')\n",
        "axs[1, 0].set_title('train_acc')\n",
        "axs[1, 1].plot(convnext_tiny_stats['test_acc'], label='convnext-t')\n",
        "axs[1, 1].plot(convnext_dcls_gauss_tiny_stats['test_acc'], label='convnext-dcls-t')\n",
        "axs[1, 1].set_title('test_acc')\n",
        "\n",
        "for ax in axs.flat[:1]:\n",
        "    ax.set(xlabel='epochs', ylabel='loss')\n",
        "for ax in axs.flat[2:]:\n",
        "    ax.set(xlabel='epochs', ylabel='top1-acc')\n",
        "\n",
        "# Hide x labels and tick labels for top plots and y ticks for right plots.\n",
        "for ax in axs.flat:\n",
        "    ax.label_outer()\n",
        "\n",
        "ax.legend()\n",
        "fig.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "n8KGpWEEUfI1"
      },
      "source": [
        "# Conclusion\n",
        "Based on the observations from the last plot, it is evident that the pretrained ConvNeXt-T model with DCLS outperforms the pretrained baseline in terms of train and test losses as well as test accuracy for the fine-tuning task on STL-10, while having the same number of trainable parameters.\n",
        "\n",
        "Additionally, it is worth noting that the ConvNeXt-T model took approximately 205 seconds on a single T4 gpu to complete the 10 fine-tuning epochs, while the ConvNeXt-DCLS-T model took around 310 seconds to achieve the same. This discrepancy is attributed to the larger kernel size utilized in the DCLS method (23 by 23 compared to the baseline's 7 by 7).\n",
        "\n",
        "To achieve a similar throughput, and as raised by the warning: \"WARNING:root:DepthWiseConv2dImplicitGEMM not installed, switching to native conv2d\",  it is recommended  to install the DepthWiseConv2dImplicitGEMM, developed by [MegEngine](https://github.com/MegEngine/cutlass), instead of the native PyTorch Conv2d. To install and enable the DepthWiseConv2dImplicitGEMM, please follow the instructions of [RepLKNet](https://github.com/DingXiaoH/RepLKNet-pytorch#use-our-efficient-large-kernel-convolution-with-pytorch)."
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.8.10"
    },
    "colab": {
      "provenance": [],
      "gpuType": "T4"
    },
    "accelerator": "GPU"
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
